using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Threading;
using static LightCAD.MathLib.Constants;

namespace LightCAD.Three
{
    public interface IImage
    {

        int width { get; set; }
        int height { get; set; }
        int depth { get; set; }
        IImage cloneImage();
    }
    public class Texture : EventDispatcher, IImage, IDispose
    {
        #region scope properties or methods
        //private static int textureId = 0;
        private static ConcurrentDictionary<int, int> objIndex = new ConcurrentDictionary<int, int>();

        #endregion

        #region Properties

        public const Image DEFAULT_IMAGE = null;
        public const int DEFAULT_MAPPING = UVMapping;
        public const double DEFAULT_ANISOTROPY = 1;


        public int id;
        public string uuid;
        public string name;
        public Source source;
        public ListEx<Image> mipmaps;
        public int mapping;
        public int wrapS;
        public int wrapT;
        public int magFilter;
        public int minFilter;
        public double anisotropy;
        public int format;
        public string internalFormat;
        public int type;
        public Vector2 offset;
        public Vector2 repeat;
        public Vector2 center;
        public double rotation;
        public bool matrixAutoUpdate;
        public Matrix3 matrix;
        public bool generateMipmaps;
        public bool premultiplyAlpha;
        public bool flipY;
        public int unpackAlignment;
        public int encoding;
        public int channel;

        public JsObj<object> userData;
        public int version;
        public Action<Texture> onUpdateAction;

        public bool isRenderTargetTexture;
        public bool needsPMREMUpdate;

        #endregion

        #region constructor
        public Texture(object images = null,
                        int mapping = Texture.DEFAULT_MAPPING,
                        int wrapS = ClampToEdgeWrapping,
                        int wrapT = ClampToEdgeWrapping,
                        int magFilter = LinearFilter,
                        int minFilter = LinearMipmapLinearFilter,
                        int format = RGBAFormat,
                        int type = UnsignedByteType,
                        double anisotropy = Texture.DEFAULT_ANISOTROPY,
                        int encoding = LinearEncoding)
        {

            this.id = ThreeThreadContext.NewId(objIndex);
            this.uuid = MathEx.GenerateUUID();
            this.name = "";

            var _images = new ListEx<IImage>();
            if (images == null)
            {
                _images.Push(Texture.DEFAULT_IMAGE);
            }
            else if (images is Image)
            {
                _images.Push((Image)images);
            }
            else if (images is IList)
            {
                //this.images.AddRange((Image[])images);
                _images.AddRange(((IList)images).Cast<IImage>());
            }
            //else if (images is Control)
            //{
            //    var ctrl = (images as Control);
            //    Bitmap b = new Bitmap(ctrl.Width, ctrl.Height);
            //    ctrl.DrawToBitmap(b, new Rectangle(0, 0, b.Width, b.Height));
            //    var img = new Image() { width = ctrl.Width, height = ctrl.Height, bmp = b };
            //    this.images.Add(img);
            //}
            else
            {
                throw new Exception("images is not Image or Image[].");
            }

            this.source = new Source(_images.ToArray());
            this.mipmaps = new ListEx<Image>();
            this.mapping = mapping == 0 ? Texture.DEFAULT_MAPPING : mapping;
            this.wrapS = wrapS == 0 ? ClampToEdgeWrapping : wrapS;
            this.wrapT = wrapT == 0 ? ClampToEdgeWrapping : wrapT;
            this.magFilter = magFilter == 0 ? LinearFilter : magFilter;
            this.minFilter = minFilter == 0 ? LinearMipmapLinearFilter : minFilter;
            this.anisotropy = anisotropy == 0 ? Texture.DEFAULT_ANISOTROPY : anisotropy;
            this.format = format == 0 ? RGBAFormat : format;
            this.internalFormat = null;
            this.type = type == 0 ? UnsignedByteType : type;
            this.offset = new Vector2(0, 0);
            this.repeat = new Vector2(1, 1);
            this.center = new Vector2(0, 0);
            this.rotation = 0;
            this.matrixAutoUpdate = true;
            this.matrix = new Matrix3();
            this.generateMipmaps = true;
            this.premultiplyAlpha = false;
            this.flipY = true;
            this.unpackAlignment = 4;   // valid values: 1, 2, 4, 8 (see http://www.khronos.org/opengles/sdk/docs/man/xhtml/glPixelStorei.xml)
                                        // Values of encoding !== THREE.LinearEncoding only supported on map, envMap and emissiveMap.
                                        //
                                        // Also changing the encoding after already used by a Material will not automatically make the Material
                                        // update. You need to explicitly call Material.needsUpdate to trigger it to recompile.
            this.encoding = encoding == 0 ? LinearEncoding : encoding;

            this.channel = 0;

            this.userData = new JsObj<object>();
            this.version = 0;
            this.onUpdateAction = null;
            this.isRenderTargetTexture = false; // indicates whether a texture belongs to a render target or not
            this.needsPMREMUpdate = false; // indicates whether this texture should be processed by PMREMGenerator or not (only relevant for render target textures)
        }
        #endregion

        #region properties
        public IImage image
        {
            get
            {
                return this.source.data[0];
            }
            set
            {
                this.source.data[0] = value;
            }
        }
        public ListEx<IImage> images { get => source.data; set { source.data = value; } }

        public bool needsUpdate
        {
            set
            {
                if (value)
                {
                    this.version++;
                    this.source.needsUpdate = true;
                }
            }
        }
        public int width { get; set; }
        public int height { get; set; }
        public int depth { get; set; }
        #endregion

        #region methods
        public void onUpdate(Texture texture)
        {
            this.onUpdateAction?.Invoke(texture);
        }
        public void updateMatrix()
        {
            this.matrix.SetUvTransform(this.offset.X, this.offset.Y, this.repeat.X, this.repeat.Y, this.rotation, this.center.X, this.center.Y);
        }

        public virtual Texture Clone()
        {
            return new Texture().copy(this);
        }
        public IImage cloneImage() => Clone();
        public Texture copy(Texture source)
        {
            this.name = source.name;
            this.source = source.source;
            this.mipmaps = source.mipmaps.Slice(0);
            this.mapping = source.mapping;
            this.wrapS = source.wrapS;
            this.wrapT = source.wrapT;
            this.magFilter = source.magFilter;
            this.minFilter = source.minFilter;
            this.anisotropy = source.anisotropy;
            this.format = source.format;
            this.internalFormat = source.internalFormat;
            this.type = source.type;
            this.offset.Copy(source.offset);
            this.repeat.Copy(source.repeat);
            this.center.Copy(source.center);
            this.rotation = source.rotation;
            this.matrixAutoUpdate = source.matrixAutoUpdate;
            this.matrix.Copy(source.matrix);
            this.generateMipmaps = source.generateMipmaps;
            this.premultiplyAlpha = source.premultiplyAlpha;
            this.flipY = source.flipY;
            this.unpackAlignment = source.unpackAlignment;
            this.channel = source.channel;
            this.encoding = source.encoding;
            this.userData = source.userData?.clone();
            this.needsUpdate = true;
            return this;
        }
        public virtual void dispose()
        {
            this.dispatchEvent(new EventArgs() { type = "dispose" });
        }
        public Vector2 transformUv(Vector2 uv)
        {
            if (this.mapping != UVMapping) return uv;
            uv.ApplyMatrix3(this.matrix);
            if (uv.X < 0 || uv.X > 1)
            {
                switch (this.wrapS)
                {
                    case RepeatWrapping:
                        uv.X = uv.X - Math.Floor(uv.X);
                        break;
                    case ClampToEdgeWrapping:
                        uv.X = uv.X < 0 ? 0 : 1;
                        break;
                    case MirroredRepeatWrapping:
                        if (Math.Abs(Math.Floor(uv.X) % 2) == 1)
                        {
                            uv.X = Math.Ceiling(uv.X) - uv.X;
                        }
                        else
                        {
                            uv.X = uv.X - Math.Floor(uv.X);
                        }
                        break;
                }
            }
            if (uv.Y < 0 || uv.Y > 1)
            {
                switch (this.wrapT)
                {
                    case RepeatWrapping:
                        uv.Y = uv.Y - Math.Floor(uv.Y);
                        break;
                    case ClampToEdgeWrapping:
                        uv.Y = uv.Y < 0 ? 0 : 1;
                        break;
                    case MirroredRepeatWrapping:
                        if (Math.Abs(Math.Floor(uv.Y) % 2) == 1)
                        {
                            uv.Y = Math.Ceiling(uv.Y) - uv.Y;
                        }
                        else
                        {
                            uv.Y = uv.Y - Math.Floor(uv.Y);
                        }
                        break;
                }
            }
            if (this.flipY)
            {
                uv.Y = 1 - uv.Y;
            }
            return uv;
        }

        public int? getWidth()
        {
            return null;
        }

        public int? getHeight()
        {
            return null;
        }
        #endregion
    }
}